<?php
/**
 * Created by PhpStorm.
 * Date: 2017/7/12
 * Time: 9:22
 * @author Appla
 */

namespace test;

require __DIR__ . DIRECTORY_SEPARATOR . 'CurlRequestLogger.php';
require_once __DIR__.'/../app/cron/header.php';

use Redis;
use artisan\db;
use DateTime;
use artisan\cache;
use test\CurlRequestLogger;
use util\DbUtil;

/**
 * Class HttpLogScheduler
 * @package test
 */
class HttpRequestLogDbWriter
{
    const DB_NAME = 'dts';
    const TABLE_NAME = 'tbl_interface_monit';
    const CACHE_INSTANCE_ID = 'base2';
    const CACHE_INSTANCE_DB = 2;

    const LAST_RUN_TIME_LOG_KEY = 'CurlRequestLogs:schedulerScript:lastRunTime';
    const MIN_RUN_INTERVAL = 300;
    const RECORD_DATETIME_FORMAT = 'Y-m-d H:i:s';

    const CURL_TIMEOUT_DEFAULT = 5;

    /**
     * @TODO 复用
     */
    const CACHE_KEY_TIMEOUT_TAG = 'requestTimeoutOptionLog';
    const CACHE_KEY_REQUEST_TIME_TAG = 'requestTimeCostLog';
    const CACHE_KEY_PREFIX = 'CurlRequestLogs:url:';
    const KEY_SEPARATOR = ':';

    /**
     * 小于该值时使用全部匹配
     */
    const MATCH_ALL_MAX_DB_SIZE = 200000;

    const KEY_PATTERN = 'CurlRequestLogs:url:*';

    const KEY_SCAN_SIZE = 10000;

    const UPDATE_SQL_TPL = 'INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s';

    /**
     * @var \artisan\cache\PHPredisDriver|\Redis
     */
    private static $cacheInstance;

    /**
     * @var db
     */
    private static $dbInstance;

    /**
     * @var array
     */
    private $matchedKeys = [];

    private $insertData = [];

    private $updatedKeys = [];

    private $unUsedKeys = [];

    /**
     * entry
     */
    public function run()
    {
        if(($runInterval = time() - self::getCacheInstance()->get(self::LAST_RUN_TIME_LOG_KEY)) <  self::MIN_RUN_INTERVAL){
            printf('与上次运行间隔小于%s秒, 请在%s秒后重试', self::MIN_RUN_INTERVAL, self::MIN_RUN_INTERVAL - $runInterval);
            exit(1);
        }
        $this->getTargetKeys();
        $this->prepareInsertData();
        $this->update();
        $this->cleanData();
    }

    /**
     * 更新到数据表
     * @TODO 使用 mysqli::multi_query
     */
    private function update()
    {
        if ($this->insertData) {
            $updatedKeys = [];
            foreach ($this->insertData as $key => $datum){
                $sql = $this->buildSql($datum);
                self::getDbInstance()->query($sql) && $updatedKeys[] = $key;
            }
            $this->updatedKeys = $updatedKeys;
        }
    }

    /**
     * @return array
     */
    private function prepareInsertData()
    {
        $filteredKeys = $this->filterKeys($this->matchedKeys);
        $this->unUsedKeys = array_diff($this->matchedKeys, $filteredKeys);
        $batchKeys = array_chunk($filteredKeys, 200);
        $data = [];
        foreach ($batchKeys as $keys) {
            $pipe = self::getCacheInstance()->pipeline();
            foreach ($keys as $key) {
                $pipe->hGetAll($key);
            }
            $tmpData = $pipe->exec();
            $data[] = array_combine($keys, $tmpData);
        }
        $data && $data = call_user_func_array('array_merge', $data);
        $tmpInsertData = [];
        foreach ($data as $key => $datum) {
            $tmpInsertData[$key] = $this->formatData($datum, $key);
        }
        return $this->insertData = $tmpInsertData;
    }


//CREATE TABLE `tbl_interface_monit` (
//`id` bigint(20) NOT NULL AUTO_INCREMENT,
//`url` varchar(50) DEFAULT NULL COMMENT '接口名',
//`request_count` int(11) DEFAULT '0' COMMENT '总请求次数',
//`http_200` int(11) DEFAULT '0' COMMENT 'http 200次数',
//`http_2xx` int(11) DEFAULT NULL COMMENT 'http 2xx次数',
//`http_4xx` int(11) DEFAULT '0' COMMENT 'http 4xx次数',
//`http_5xx` int(11) DEFAULT '0' COMMENT 'http 5xx次数',
//`timeout` int(11) DEFAULT '0' COMMENT '超时时间',
//`request_timeout_error` int(11) DEFAULT '0' COMMENT '请求超时错误次数',
//`response_format_error` int(11) DEFAULT '0' COMMENT '返回格式错误次数',
//`record_time` datetime DEFAULT NULL COMMENT '记录时间',
//`create_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '插入记录时间',
//PRIMARY KEY (`id`),
//UNIQUE KEY `url` (`url`,`record_time`) USING BTREE,
//KEY `create_at` (`create_at`) USING BTREE
//) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='接口监控记录表';

    /**
     * @param array $data
     * @return array
     * @see DB.dts.tbl_interface_monit
     */
    private function formatData(array $data, $key)
    {
        $formattedData = [];
        if($data && $key) {
            list($url, $time) = self::parseKey($key);
            $formattedData = [
                'url' => $url,
                'request_count' => $data['total_hit'],
                'request_time' => $data['time_cost'],
                'http_200' => isset($data['status_code_200']) ? $data['status_code_200'] : 0,
                'http_2xx' => $this->arrayCountByPattern($data, 'status_code_2\d{2}'),
                'http_4xx' => $this->arrayCountByPattern($data, 'status_code_4\d{2}'),
                'http_5xx' => $this->arrayCountByPattern($data, 'status_code_5\d{2}'),
                'timeout' => isset($data['timeout']) ? $data['timeout'] : self::CURL_TIMEOUT_DEFAULT,
                'request_timeout_error' => isset($data['status_code_28']) ? $data['status_code_28'] : 0, //curl timeout error
                'response_format_error' => isset($data['invalid_xml_or_json']) ? $data['invalid_xml_or_json'] : 0,
                'record_time' => (new DateTime($time))->format(self::RECORD_DATETIME_FORMAT),
            ];
        }
        return $formattedData;
    }

    /**
     * @param string $key
     * @return array
     * @TODO 性能相近,适当的时候只用正则
     */
    private static function parseKey($key)
    {
        static $prefixLen;
        if ($prefixLen === null) {
            $prefixLen = strlen(self::CACHE_KEY_PREFIX);
        }
        $key = substr($key, $prefixLen);
        if (preg_match('~^(.*)'. self::KEY_SEPARATOR .'(\d{12})$~', $key, $match)) {
           return array_slice($match, 1, 2);
        }
        if(substr_count($key, self::KEY_SEPARATOR) > 1) {
            $tmp = explode(self::KEY_SEPARATOR, $key);
            $time = array_splice($tmp, -1, 1);
            $ret = [implode(self::KEY_SEPARATOR, $tmp), $time];
        } else {
            $ret = explode(self::KEY_SEPARATOR, $key, 2);
        }
        return $ret;
    }

    /**
     * @param array $data
     * @return string
     * @TODO 根据表字段过滤输入字段, 可使用shop2.DbUtil::filterInsertDataFields
     */
    private function buildSql(array $data)
    {
        $keys = array_keys($data);
        $values = array_values($data);
        return sprintf(self::UPDATE_SQL_TPL, self::TABLE_NAME, implode(',', $keys), sprintf('"%s"', implode('","', $values)), $this->buildUpdateSql($data));
    }

    /**
     * @param array $data
     * @param callable|null $func
     * @return string
     */
    private function buildUpdateSql(array $data, callable $func = null)
    {
        $updateItem = [];
        $func || $func = function($key, $val){
            return sprintf('%s = %s+%d', $key, $key, $val);
        };
        unset($data['url'], $data['record_time']);
        foreach ($data as $key => $val) {
            is_numeric($val) && $updateItem[] = $func($key, $val);
        }
        return implode(',', $updateItem);
    }

    /**
     * @param array $data
     * @param string $pattern
     * @return int
     * @TODO 已知前对和正确长度,据此可提高性能 : status_code_xxx
     */
    private function arrayCountByPattern(array $data, $pattern)
    {
        $total = 0;
        if($data && $pattern){
            $pattern = sprintf('~%s~', $pattern);
            foreach ($data as $key => $val) {
//                if(strlen($key) === 15) {
//                    strpos($key, $pattern) === 0 && $total += $val;
//                }
                preg_match($pattern, $key) && $total += $val;
            }
        }
        return $total;
    }

    /**
     * 过滤耗时记录和超时设置记录
     * @param array $keys
     * @return array
     */
    private function filterKeys(array $keys)
    {
        return array_filter($keys, function($key){
            return strpos($key, self::CACHE_KEY_TIMEOUT_TAG) === false && strpos($key, self::CACHE_KEY_REQUEST_TIME_TAG) === false;
        });
    }

    /**
     * @return array|mixed
     */
    private function getTargetKeys()
    {
        $localCacheInstance = self::getCacheInstance();
        $dbSize = $localCacheInstance->dbSize();
        $keys = [];
        if($dbSize > self::MATCH_ALL_MAX_DB_SIZE) {
            $cursor = null;
            while($cursor !== 0) {
                $keys[] = $localCacheInstance->scan($cursor, self::KEY_PATTERN, self::KEY_SCAN_SIZE);
            }
            $keys = call_user_func_array('array_merge', $keys);
//            $keys = array_merge(...$keys);
        } else {
            $keys = self::getCacheInstance()->keys(self::KEY_PATTERN);
        }
        return $this->matchedKeys = $keys;
    }

    /**
     * @return int
     */
    private function cleanData()
    {
        return self::getCacheInstance()->del(array_merge($this->updatedKeys, $this->unUsedKeys));
    }

    /**
     * @return \artisan\cache\PHPredisDriver|\Redis
     */
    private static function getCacheInstance()
    {
        return self::$cacheInstance ?: self::$cacheInstance = (function () {
            $redis = new Redis();
            $redis->connect(...CurlRequestLogger::REDIS_CONFIG);
            $redis->select(self::CACHE_INSTANCE_DB);
            return $redis;
        })();
        //        return self::$cacheInstance ?: self::$cacheInstance = cache::connect(self::CACHE_INSTANCE_ID);
    }

    /**
     * @return db
     */
    private static function getDbInstance()
    {
//        return self::$dbInstance ?: self::$dbInstance = db::connect(self::DB_NAME);
        return self::$dbInstance ?: self::$dbInstance = DbUtil::getDbInstance('localLog');
    }
}

(new HttpRequestLogDbWriter())->run();